
package cn.jh.common.core.data.redis;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.reflect.TypeToken;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scripting.support.ResourceScriptSource;

import java.util.List;

import static com.google.common.base.Preconditions.checkArgument;


/**
 * The common redis lua scripts wrapper {@code RedisScript} factory.
 *
 * @author Fuchun
 * @since 1.0
 */
public class RedisScripts {

    private static final String SN_RELEASE_LOCK = "release_lock";
    private static final String SN_SET_LOCK_WITH_TIMEOUT = "set_lock_with_timeout";
    private static final String SN_GET_MODEL_BY_INDEX = "get_model_by_index";
    private static final String SN_GET_MODEL_BY_HASH_INDEX = "get_model_by_hash_index";
    private static final String SN_GET_LIST_BY_IDS = "get_list_by_ids";
    private static final String SN_GET_LIST_BY_HASH_INDEX = "get_list_by_hash_index";
    private static final String SN_BULK_POP = "bulk_pop";

    @SuppressWarnings("unchecked")
    public static final Class<List<String>> LIST_STRING_CLASS = (Class<List<String>>)
            new TypeToken<List<String>>() {}.getRawType();

    private static final Table<Class<?>, String, RedisScript<?>> REDIS_SCRIPT_TABLE = HashBasedTable.create();

    static {
        register(Boolean.class, SN_SET_LOCK_WITH_TIMEOUT, "/META-INF/scripts/set_lock_with_timeout.lua");
        register(Boolean.class, SN_RELEASE_LOCK, "/META-INF/scripts/release_lock.lua");
        register(String.class, SN_GET_MODEL_BY_INDEX, "/META-INF/scripts/get_model_by_index.lua");
        register(String.class, SN_GET_MODEL_BY_HASH_INDEX, "/META-INF/scripts/get_model_by_hash_index.lua");
        register(List.class, SN_GET_LIST_BY_IDS, "/META-INF/scripts/get_list_by_ids.lua");
        register(List.class, SN_GET_LIST_BY_HASH_INDEX, "/META-INF/scripts/get_list_by_hash_index.lua");
        register(List.class, SN_BULK_POP, "/META-INF/scripts/bulk_pop.lua");
    }

    @NotNull
    public static RedisScript<Boolean> getReleaseLockScript() {
        return getRedisScript(Boolean.class, SN_RELEASE_LOCK);
    }

    @NotNull
    public static RedisScript<Boolean> getSetLockWithTimeoutScript() {
        return getRedisScript(Boolean.class, SN_SET_LOCK_WITH_TIMEOUT);
    }

    @NotNull
    public static RedisScript<String> getModelByIndexScript() {
        return getRedisScript(String.class, SN_GET_MODEL_BY_INDEX);
    }

    @NotNull
    public static RedisScript<String> getModelByHashIndexScript() {
        return getRedisScript(String.class, SN_GET_MODEL_BY_HASH_INDEX);
    }

    @NotNull
    public static RedisScript<List<String>> getListByIdsScript() {
        return getRedisScript(LIST_STRING_CLASS, SN_GET_LIST_BY_IDS);
    }

    @NotNull
    public static RedisScript<List<String>> getListByHashIndexScript() {
        return getRedisScript(LIST_STRING_CLASS, SN_GET_LIST_BY_HASH_INDEX);
    }

    @NotNull
    public static RedisScript<List<String>> getBulkPopScript() {
        return getRedisScript(LIST_STRING_CLASS, SN_BULK_POP);
    }

    /**
     * 返回指定返回类型和脚本名称的{@code redis lua} 脚本实例。
     *
     * @param resultType 模型或实体类型。
     * @param scriptName 脚本的名称。
     * @param <T> 模型或实体类型。
     * @return 返回指定返回类型和脚本名称的{@code redis lua} 脚本实例。
     * @throws IllegalArgumentException if {@code resultType == null or scriptName == null}
     */
    public static <T> RedisScript<T> getRedisScript(Class<T> resultType, String scriptName) {
        checkArgument(resultType != null, "resultType");
        checkArgument(scriptName != null, "scriptName");
        @SuppressWarnings("unchecked")
        RedisScript<T> redisScript = (RedisScript<T>) REDIS_SCRIPT_TABLE.get(resultType, scriptName);
        return redisScript;
    }

    /**
     * 构建并添加注册一个指定返回类型的{@code redis lua} 脚本实例。
     * <pre> {@code
     * RedisScript<Boolean> rs = RedisScripts.register(Boolean.class, "set_lock_with_timeout",
     *         "/META-INF/scripts/set_lock_with_timeout.lua");
     * // Next time, you can get it:
     * RedisScript<Boolean> rs2 = RedisScripts.getRedisScript(Boolean.class, "set_lock_with_timeout");
     * assert rs == rs2;
     * }</pre>
     *
     * @param resultType 脚本执行返回的类型。
     * @param scriptName 脚本名称。
     * @param classResource 脚本资源所在路径（script classpath）
     * @param <T> 脚本执行返回的类型。
     * @return 返回一个指定返回类型的{@code redis lua} 脚本实例。
     * @throws IllegalArgumentException 如果给定的参数为{@code null}
     */
    public static <T> RedisScript<T> register(Class<T> resultType, String scriptName, String classResource) {
        checkArgument(scriptName != null, "scriptName");
        RedisScript<T> redisScript = buildFromResource(resultType, classResource);
        REDIS_SCRIPT_TABLE.put(resultType, scriptName, redisScript);
        return redisScript;
    }

    /**
     * 构建并返回指定返回类型的{@code redis lua} 脚本实例。
     * <pre> {@code
     * String classResource = "/META-INF/scripts/get_model_by_index.lua";
     * RedisScript<User> userRedisScript = buildFromResource(User.class, classResource);
     * }</pre>
     *
     * @param resultType 脚本执行返回的类型。
     * @param classResource   脚本资源所在路径（script classpath）
     * @param <E>        脚本执行返回的类型。
     * @return 返回指定返回类型的{@code redis lua} 脚本实例。
     * @throws IllegalArgumentException if {@code resultType == null or classResource == null}
     */
    @Contract("null, _ -> fail; _, null -> fail")
    public static <E> RedisScript<E> buildFromResource(Class<E> resultType, String classResource) {
        checkArgument(resultType != null, "resultType");
        checkArgument(classResource != null, "classResource");
        DefaultRedisScript<E> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(resultType);
        redisScript.setScriptSource(new ResourceScriptSource(
                new ClassPathResource(classResource)));
        return redisScript;
    }

    /**
     * 构建并返回指定返回类型和脚本文体的{@code redis lua} 脚本实例。
     * <pre> {@code
     * String script = "return redis.call('GET', KEYS[1])";
     * RedisScript<String> stringRedisScript = newRedisScript(String.class, script);
     * }</pre>
     *
     * @param resultType 脚本执行返回的类型。
     * @param scriptText redis lua script text.
     * @param <E> 脚本执行返回的类型。
     * @return 返回指定返回类型和脚本文体的{@code redis lua} 脚本实例。
     * @throws IllegalArgumentException if {@code resultType == null || scriptText == null}
     */
    @Contract("null, _ -> fail; _, null -> fail")
    public static <E> RedisScript<E> newRedisScript(Class<E> resultType, String scriptText) {
        checkArgument(resultType != null, "resultType");
        checkArgument(scriptText != null, "scriptText");
        DefaultRedisScript<E> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(resultType);
        redisScript.setScriptText(scriptText);
        return redisScript;
    }
}
